using System;
using System.IO;
using System.Xml;
using System.Collections.Generic;
using System.Diagnostics;

namespace Dida
{
	public class MeshLoader
	{
		List<Material> materials = new List<Material>();
		List<Triangle> triangles = new List<Triangle>();

		/// <summary>
		/// Reads the mesh from the given stream. The file format of
		/// the stream is determined automatically.
		/// </summary>
		/// <param name="s">The stream to read the mesh from.</param>
		public MeshLoader(Stream s)
		{
			Create(s);
		}

		public MeshLoader(string srcFileName)
		{
			using (FileStream s = new FileStream(srcFileName, FileMode.Open))
			{
				Create(s);
			}
		}

		private void Create(Stream s)
		{
			BinaryReader reader = new BinaryReader(s);
			if (IsBinaryMesh(s, reader))
			{
				ReadFromBinary(reader);
			}
			else
			{
				XmlReaderSettings settings = new XmlReaderSettings();
				settings.IgnoreComments = true;
				settings.IgnoreWhitespace = true;
				XmlReader xmlReader = XmlReader.Create(s,settings);
				ReadFromXml(xmlReader);
			}
		}

		private static bool IsBinaryMesh(Stream s,BinaryReader reader)
		{
			byte[] bytes = reader.ReadBytes(8);
			char[] magicNumber = "DidaMesh".ToCharArray();
			for(int i = 0;i < 8;i++)
				if(bytes[i] != magicNumber[i])
				{
					s.Seek(-8,SeekOrigin.Current);
					return false;
				}
			return true;
		}

		private void ReadFromBinary(BinaryReader reader)
		{
			long streamLen = reader.BaseStream.Length;

			int nMaterials = reader.ReadInt32();
			// if there are more materials than the length of the stream,
			// then the file is corrupted for sure.
			if (nMaterials < 0 || nMaterials > streamLen)
				throw new BinaryFormatException();

			materials.Capacity = nMaterials;
			for (int i = 0; i < nMaterials; i++)
				materials.Add(new Material(this, reader));

			int nTris = reader.ReadInt32();
			// if there are more triangles than the length of the stream,
			// then the file is corrupted for sure.
			if (nTris < 0 || nTris > streamLen)
				throw new BinaryFormatException();

			triangles.Capacity = nTris;
			for (int i = 0; i < nTris; i++)
				triangles.Add(new Triangle(reader));

			foreach (Material mat in materials)
			{
				foreach (Triangle tri in mat.Triangles)
					tri.mat = mat;

				if (mat.firstTri > nTris - mat.nTris)
					throw new BinaryFormatException();
			}
		}

		private void ReadFromXml(XmlReader reader)
		{
			reader.ReadStartElement("didaMesh");
			while (reader.IsStartElement())
			{
				string name = reader.GetAttribute("name");
				if (name == null)
					throw new XmlAttributeNotSpecifiedException(reader, "name");
				reader.ReadStartElement("material");
				
				Material mat = new Material(this,name);
				materials.Add(mat);

				int first = triangles.Count;
				int num = 0;
				while (reader.IsStartElement())
				{
					Triangle tri = new Triangle(mat,reader);
					triangles.Add(tri);
					num++;
				}
				mat.firstTri = first;
				mat.nTris = num;

				reader.ReadEndElement();
			}
			reader.ReadEndElement();
		}

		public IList<Triangle> Triangles
		{
			get { return triangles; }
		}

		public IList<Material> Materials
		{
			get { return materials; }
		}

		public class Material
		{
			internal MeshLoader loader;
			internal string name;
			internal int firstTri;
			internal int nTris;

			internal Material(MeshLoader loader,string name)
			{
				this.loader = loader;
				this.name = name;
			}

			internal Material(MeshLoader loader, BinaryReader reader)
			{
				this.loader = loader;

				name = reader.ReadString();
				firstTri = reader.ReadInt32();
				nTris = reader.ReadInt32();

				if (firstTri < 0)
					throw new BinaryFormatException();
				if (nTris < 0)
					throw new BinaryFormatException();
			}

			public string Name
			{
				get { return name; }
			}

			public IList<Triangle> Triangles
			{
				get
				{
					return new MaterialTrianglesList(loader, firstTri, nTris);
				}
			}

			public int FirstTriangleIndex
			{
				get { return firstTri; }
			}

			public int NTriangles
			{
				get { return nTris; }
			}
		}

		private class MaterialTrianglesList : IList<Triangle>
		{
			MeshLoader meshLoader;
			int firstTri;
			int nTris;

			public MaterialTrianglesList(MeshLoader meshLoader, int firstTri, int nTris)
			{
				this.meshLoader = meshLoader;
				this.firstTri = firstTri;
				this.nTris = nTris;
			}

			public int IndexOf(Triangle item)
			{
				for(int i = 0;i < nTris;i++)
					if(meshLoader.Triangles[i + firstTri] == item)
						return i;
				return -1;
			}

			public void Insert(int index, Triangle item)
			{
				Debug.Fail("This is a read only list");
			}

			public void RemoveAt(int index)
			{
				Debug.Fail("This is a read only list");
			}

			public Triangle this[int index]
			{
				get
				{
					Debug.Assert(index >= firstTri && index < firstTri + nTris);
					return meshLoader.Triangles[index + firstTri];
				}

				set
				{
					Debug.Fail("This is a read only list");
				}
			}

			public void Add(Triangle item)
			{
				Debug.Fail("This is a read only collection");
			}

			public void Clear()
			{
				Debug.Fail("This is a read only collection");
			}

			public bool Contains(Triangle item)
			{
				for (int i = 0; i < nTris; i++)
					if (meshLoader.Triangles[firstTri + i] == item)
						return true;
				return false;
			}

			public void CopyTo(Triangle[] array, int arrayIndex)
			{
				IList<Triangle> allTris = meshLoader.Triangles;
				for (int i = 0; i < nTris; i++)
					array[arrayIndex + i] = allTris[firstTri + i];
			}

			public int Count
			{
				get { return nTris; }
			}

			public bool IsReadOnly
			{
				get { return true; }
			}

			public bool Remove(Triangle item)
			{
				Debug.Fail("This is a read only collection");
				return false;
			}

			public IEnumerator<Triangle> GetEnumerator()
			{
				IList<Triangle> tri = meshLoader.Triangles;
				return new MaterialTrianglesEnumerator(tri, firstTri, firstTri + nTris);
			}

			System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
			{
				return GetEnumerator();
			}
		}

		private class MaterialTrianglesEnumerator : IEnumerator<Triangle>
		{
			IList<Triangle> allTriangles;
			int first;
			int curIndex;
			int end;

			public MaterialTrianglesEnumerator(IList<Triangle> allTriangles,
				int first, int end)
			{
				this.allTriangles = allTriangles;
				this.first = first;
				this.curIndex = first - 1;;
				this.end = end;
			}

			public Triangle Current
			{
				get
				{
					Debug.Assert(curIndex >= first,"You have to call MoveNext before you can access Current");
					return allTriangles[curIndex]; 
				}
			}

			public void Dispose() {}

			object System.Collections.IEnumerator.Current
			{
				get 
				{
					Debug.Assert(curIndex >= first,"You have to call MoveNext before you can access Current");
					return allTriangles[curIndex]; 
				}
			}

			public bool MoveNext()
			{
				curIndex++;
				return curIndex < end;
			}

			public void Reset()
			{
				curIndex = first - 1;
			}
		}

		public struct Vertex
		{
			public Vector3D pos;

			internal void Read(XmlReader reader)
			{
				string posStr = reader.GetAttribute("pos");
				if (posStr == null)
					throw new XmlAttributeNotSpecifiedException(reader, "pos");

				pos = Vector3D.Parse(posStr);
				reader.ReadElementString("vertex");
			}

			internal void Read(BinaryReader reader)
			{
				pos.x = reader.ReadSingle();
				pos.y = reader.ReadSingle();
				pos.z = reader.ReadSingle();
			}
		}

		public class Triangle
		{
			internal Vertex[] vertices = new Vertex[3];
			internal Material mat;			

			internal Triangle(Material mat,XmlReader reader)
			{
				this.mat = mat;

				reader.ReadStartElement("triangle");

				XmlPositionMarker errMarker = new XmlPositionMarker(reader);

				vertices[0].Read(reader);
				vertices[1].Read(reader);
				vertices[2].Read(reader);
				
				reader.ReadEndElement();

				// test if it's not degenerated
				if (IsDegenerated())
				{
					throw new XmlReaderException("This mesh is not valid. It contains " +
						"degenerated triangles.", errMarker);
				}
			}

			internal Triangle(BinaryReader reader)
			{
				vertices[0].Read(reader);
				vertices[1].Read(reader);
				vertices[2].Read(reader);

				// test if it's not degenerated
				if (IsDegenerated())
					throw new Exception("This mesh is not valid. It contains degenerated triangles.");
			}

			private bool IsDegenerated()
			{
				if(vertices[0].pos == vertices[1].pos ||
					vertices[1].pos == vertices[2].pos ||
					vertices[2].pos == vertices[0].pos)
				{
					return true;
				}

				return false;
			}

			public Vertex[] Vertices
			{
				get { return vertices; }
			}
		}

		public class BinaryFormatException : Exception
		{
			public override string Message
			{
				get
				{
					return "Not a valid .dmb file, or the file is corrupted";
				}
			}
		}
	}
}
